package com.zxjbyte.yiyi.framework.requestlog.aspect;

import cn.hutool.core.date.StopWatch;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.google.common.collect.Lists;
import com.zxjbyte.yiyi.framework.common.constant.Constant;
import com.zxjbyte.yiyi.framework.common.constant.StringPool;
import com.zxjbyte.yiyi.framework.common.util.ClassXUtils;
import com.zxjbyte.yiyi.framework.common.util.JsonUtil;
import com.zxjbyte.yiyi.framework.common.util.ServletXUtil;
import com.zxjbyte.yiyi.framework.requestlog.config.RequestLogProperties;
import com.zxjbyte.yiyi.framework.requestlog.enums.RequestLogLevel;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.InputStreamSource;
import org.springframework.http.HttpStatus;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.UnaryOperator;

/**
 * 方便开发时日志输出切面
 * @Author zhangxingjia
 * @Date 2023/3/9 10:53
 * @Version: 1.0
 */
@Slf4j
@Aspect
@AllArgsConstructor
public class RequestLogAspect {

    private final RequestLogProperties properties;
    private final PathMatcher antPathMatcher = Singleton.get(AntPathMatcher.class);

    /**
     * 环切控制和非静态返回值R的函数
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("(execution(!static com.zxjbyte.yiyi.framework.common.domain.api.PageResult *(..)) || " +
            "execution(!static com.zxjbyte.yiyi.framework.common.domain.api.R *(..))) && " +
            "(@within(org.springframework.stereotype.Controller) || " +
            "@within(org.springframework.web.bind.annotation.RestController))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        RequestLogLevel level = properties.getLevel();
        // NOT LOG
        if(RequestLogLevel.NONE == level){
            return joinPoint.proceed();
        }
        HttpServletRequest request = ServletXUtil.getRequest();
        if(request == null){
            return joinPoint.proceed();
        }
        String requestUrl = request.getRequestURI();
        if(properties.getExcludePaths().stream().anyMatch(path -> antPathMatcher.match(toPath(path), requestUrl))){
            return joinPoint.proceed();
        }
        String requestMethod = request.getMethod();
        // 获取请求协议的版本
        String protocolVersion = request.getProtocol();
        // 构建成一条长 日志，避免并发下日志错乱
        StringBuilder buf = new StringBuilder(300);
        // 日志参数
        List<Object> logArgs = new ArrayList<>();
        buf.append(formatLogStr("\n\n>>>> [{} {} {}] (Http Request)\n"));
        logArgs.add(requestMethod);
        logArgs.add(requestUrl);
        logArgs.add(protocolVersion);
        // 打印请求参数
        logIngArgs(joinPoint, buf, logArgs);
        // 打印请求 headers
        logIngHeaders(request, level, buf, logArgs);
        buf.append(formatLogStr("<<<< [{} {}] (Http Response)\n"));
        logArgs.add(requestMethod);
        logArgs.add(requestUrl);
        // 打印执行时间
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        Object result = null;
        try {
            result = joinPoint.proceed();
            return result;
        } finally {
            HttpServletResponse response = ServletXUtil.getResponse();
            stopWatch.stop();
            // 打印返回结构体
            buf.append("<-- {} {} {} | Duration: {} ms\n");
            logArgs.add(protocolVersion);
            int statusCode = response.getStatus();
            logArgs.add(statusCode);
            logArgs.add(getReasonPhrase(statusCode));
            logArgs.add(stopWatch.getTotalTimeMillis());
            if (RequestLogLevel.BODY.lte(level)) {
                buf.append("<-- {}\n");
                logArgs.add(JsonUtil.toJsonAsString(result));
            }
            log.info(buf.toString(), logArgs.toArray());
        }
    }

    /**
     * 根据状态码获取标准描述
     * @param statusCode HTTP 状态码
     * @return 状态描述，如 "OK", "Not Found"
     */
    public static String getReasonPhrase(int statusCode) {
        HttpStatus statusEnum = HttpStatus.resolve(statusCode);
        return (statusEnum != null ? statusEnum.getReasonPhrase() : "");
    }

    public String toPath(final String path){
        return translator().apply(path);
    }

    private UnaryOperator<String> translator(){
        return (path) -> {
            String path2 = StrUtil.addPrefixIfNot(path, "/");
            if(Constant.PATTERN_GLOB.matcher(path2).matches()){
                return path2;
            }
            String contextPath = SpringUtil.getProperty("server.servlet.context-path");
            if(StrUtil.isBlank(contextPath)){
                return path2;
            }
            List<String> list = StrUtil.splitTrim(path2, "/");
            String s0 = list.get(0);
            if(!Objects.equals(s0, contextPath)){
                list.set(0, "/" + contextPath + "/" + s0);
                path2 = String.join("/", list);
            }
            return path2;
        };
    }

    /**
     * 记录请求参数
     *
     * @param point         ProceedingJoinPoint
     * @param buf  StringBuilder
     * @param logArgs logArgs
     */
    public void logIngArgs(ProceedingJoinPoint point, StringBuilder buf, List<Object> logArgs) {
        MethodSignature ms = (MethodSignature) point.getSignature();
        Method method = ms.getMethod();
        Object[] args = point.getArgs();
        // 请求参数处理
        final Map<String, Object> paraMap = new HashMap<>(16);
        // 一次请求只能有一个 request body
        Object requestBodyValue = null;
        // request 获取的form参数
        List<Map<String, String[]>> parameterMapList = Lists.newArrayListWithExpectedSize(16);
        boolean isValueSerialize = false;
        for (int i = 0; i < args.length; i++) {
            // 读取方法参数
            MethodParameter methodParam = ClassXUtils.getMethodParameter(method, i);
            // PathVariable 参数跳过
            PathVariable pathVariable = methodParam.getParameterAnnotation(PathVariable.class);
            if (pathVariable != null) {
                continue;
            }
            RequestBody requestBody = methodParam.getParameterAnnotation(RequestBody.class);
            String parameterName = methodParam.getParameterName();
            Object value = args[i];
            // 如果是body的json则是对象
            if (requestBody != null) {
                requestBodyValue = value;
                continue;
            }
            // 处理 参数
            if (value instanceof HttpServletRequest) {
                parameterMapList.add(((HttpServletRequest) value).getParameterMap());
                continue;
            } else if (value instanceof WebRequest) {
                parameterMapList.add(((WebRequest) value).getParameterMap());
                continue;
            } else if (value instanceof HttpServletResponse) {
                continue;
            } else if (value instanceof MultipartFile) {
                MultipartFile multipartFile = (MultipartFile) value;
                String name = multipartFile.getName();
                String fileName = multipartFile.getOriginalFilename();
                paraMap.put(name, fileName);
                continue;
            } else if (value instanceof MultipartFile[]) {
                MultipartFile[] arr = (MultipartFile[]) value;
                if (arr.length == 0) {
                    continue;
                }
                String name = arr[0].getName();
                StringBuilder sb = new StringBuilder(arr.length);
                for (int j = 0; j < arr.length; j++) {
                    if(j > 0){
                        sb.append(StringPool.COMMA);
                    }
                    MultipartFile multipartFile = arr[j];
                    sb.append(multipartFile.getOriginalFilename());
                }
                paraMap.put(name, sb.toString());
                continue;
            } else if (value instanceof List) {
                List<?> list = (List<?>) value;
                AtomicBoolean isSkip = new AtomicBoolean(false);
                for (Object o : list) {
                    if ("StandardMultipartFile".equalsIgnoreCase(o.getClass().getSimpleName())) {
                        isSkip.set(true);
                        break;
                    }
                }
                if (isSkip.get()) {
                    paraMap.put(parameterName, "此参数不能序列化为JSON");
                    continue;
                }
            }
            // 参数名
            RequestParam requestParam = methodParam.getParameterAnnotation(RequestParam.class);
            String paraName = parameterName;
            if (requestParam != null && StrUtil.isNotBlank(requestParam.value())) {
                paraName = requestParam.value();
            }
            if (value == null) {
                paraMap.put(paraName, null);
            } else if (ClassXUtils.isPrimitiveOrWrapper(value.getClass())) {
                paraMap.put(paraName, value);
            } else if (value instanceof InputStream) {
                paraMap.put(paraName, "InputStream");
            } else if (value instanceof InputStreamSource) {
                paraMap.put(paraName, "InputStreamSource");
            } else if (JsonUtil.canSerialize(value)) {
                // 判断模型能被 json 序列化，则添加
                paraMap.put(paraName, value);
                isValueSerialize = true;
            } else {
                paraMap.put(paraName, "此参数不能序列化为JSON");
            }
        }
        if(!isValueSerialize && !parameterMapList.isEmpty()){
            parameterMapList.forEach(paraMap::putAll);
        }
        // 请求参数
        if (paraMap.isEmpty()) {
            buf.append("\n");
        } else {
            buf.append("--> [Params]  {}\n");
            logArgs.add(JsonUtil.toJsonAsString(paraMap));
        }
        if (requestBodyValue != null) {
            buf.append("--> [Body]  {}\n");
            logArgs.add(JsonUtil.toJsonAsString(requestBodyValue));
        }
    }

    /**
     * 记录请求头
     *
     * @param request       HttpServletRequest
     * @param level         日志级别
     * @param buf  StringBuilder
     * @param logArgs logArgs
     */
    public void logIngHeaders(HttpServletRequest request, RequestLogLevel level,
                              StringBuilder buf, List<Object> logArgs) {
        // 打印请求头
        if (RequestLogLevel.HEADERS.lte(level)) {
            Enumeration<String> headers = request.getHeaderNames();
            while (headers.hasMoreElements()) {
                String headerName = headers.nextElement();
                String headerValue = request.getHeader(headerName);
                buf.append("--> [Header]  {}: {}\n");
                logArgs.add(headerName);
                logArgs.add(headerValue);
            }
        }
    }

    public String formatLogStr(String content){
        return String.format("\u001B[32m%s\u001B[0m", content);
    }
}
